{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "d9674b79-f527-49d6-8d31-c59e2322c0ed",
   "metadata": {},
   "source": [
    "# Project 2048\n",
    "\n",
    "In this project, you will develop the **2048** game in Python using a functional (without class) approach and two libraries for the graphical interface: `rich` to get a nice display in console and `readchar` for the keyboard input.\n",
    "\n",
    "## Objectives\n",
    "\n",
    "- Understand functional programming in Python\n",
    "- Work with data structures\n",
    "- Use the Blessed library to create an interactive terminal interface\n",
    "- Decompose a problem into simple and testable functions\n",
    "\n",
    "## 2048 Game Rules Review\n",
    "\n",
    "On a 4x4 grid, a new tile (2 or 4) appears randomly each turn (2 appears with 90% of chance, 4 with 10%). The player can move all tiles in four directions (up, down, left, right). Two adjacent tiles of the same value merge into one (double their value). The game ends when no moves are possible. The goal is to reach tile 2048 (and continue beyond).\n",
    "\n",
    "You can test the game at this address: [2048](https://jeu2048.fr/).\n",
    "\n",
    "# Instructions\n",
    "\n",
    "You must follow the tutorial for this project. It will provide you with a framework, particularly for the MVC pattern (Model, View, Controller). Follow this framework meticulously. The minimum objective of the project is to achieve a playable version with a text-based interface (see part 4). For more advanced students, it will be possible to add optional features (Tkinter interface, Pygame, AI, game saving, high scores, etc.) to improve your final grade.\n",
    "\n",
    "# Part 1 - Model\n",
    "\n",
    "The model will consist of pure functions that manipulate the game state, represented by a dictionary `state`.\n",
    "\n",
    "## Step 1.1: Data Structure and Initialization\n",
    "\n",
    "Goal: Create functions to initialize and manipulate the game state.\n",
    "\n",
    "**Game State Structure:**\n",
    "\n",
    "```\n",
    "{\n",
    "    'grid': [[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]],\n",
    "    'score': 0,\n",
    "    'game_over': False,\n",
    "    'size': 4\n",
    "}\n",
    "```\n",
    "\n",
    "**To Implement:**"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 39,
   "id": "d4474ccb-ed74-445a-986a-a23e95cec564",
   "metadata": {},
   "outputs": [],
   "source": [
    "def create_game_state(size=4):\n",
    "    \"\"\"\n",
    "    Creates a new initial game state.\n",
    "\n",
    "    Args:\n",
    "        size (int): Grid size (default 4)\n",
    "\n",
    "    Returns:\n",
    "        dict: Initial game state\n",
    "    \"\"\"\n",
    "    pass\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "8bd0c2d8-5319-46e6-92d8-25ad2813794a",
   "metadata": {},
   "source": [
    "**Tests to Perform:**\n",
    "\n",
    "Use the following code to test your code. Always remove the tests after.\n",
    "\n",
    "```python\n",
    "state = create_game_state()\n",
    "print(state[\"grid\"])  # -> [[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]]\n",
    "print(state[\"score\"]) # -> 0\n",
    "print(state[\"game_over\"]) # -> False\n",
    "\n",
    "state = create_game_state(size=5)\n",
    "print(state[\"grid\"]) # -> [[0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0]]\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "3647ffbc-738b-40de-b6e8-e1e7e56fa769",
   "metadata": {},
   "source": [
    "## Step 1.2: Adding Random Tiles\n",
    "\n",
    "**Goal:** Implement functions to add a new tile. Remember to correctly organized your source code. Please ensure you adhere to the function parameters and expected return values. Everything is specified in the comment at the beginning of the function. Please keep it in your code; it is part of the documentation.\n",
    "\n",
    "**To Implement:** Implement the following functions to add a tile.\n",
    "\n",
    "**Hint:** Becarefull and keep the same order to identify a cell. I recommand first, the line, and then the column. So `grid[0][2]` means the tile in the first line and the last column."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 34,
   "id": "a7d6eb36-b85a-4dd1-86a7-5176067459ad",
   "metadata": {},
   "outputs": [],
   "source": [
    "import random\n",
    "\n",
    "def get_empty_cells(grid):\n",
    "    \"\"\"\n",
    "    Returns the list of coordinates of empty cells.\n",
    "    \n",
    "    Args:\n",
    "        grid (list): Game grid\n",
    "    \n",
    "    Returns:\n",
    "        list: List of tuples (row, column)\n",
    "    \"\"\"\n",
    "    pass\n",
    "\n",
    "def add_random_tile(state):\n",
    "    \"\"\"\n",
    "    Add a random tile (2 or 4) to an empty cell in the grid.\n",
    "\n",
    "    Args:\n",
    "        state (dict): Current game state\n",
    "\n",
    "    Returns:\n",
    "        bool: True if a tile was added, False otherwise\n",
    "    \"\"\"\n",
    "    pass\n",
    "\n",
    "def update_score(state):\n",
    "    \"\"\"\n",
    "    Updates the score in the game state. The score is the sum of all tiles.\n",
    "\n",
    "    Args:\n",
    "        state (dict): Current game state\n",
    "\n",
    "    Returns:\n",
    "        None\n",
    "    \"\"\"\n",
    "    "
   ]
  },
  {
   "cell_type": "markdown",
   "id": "23859e19-6d81-45d4-ac30-5b8bb0b9416e",
   "metadata": {},
   "source": [
    "**Tests to Perform:**\n",
    "\n",
    "```python\n",
    "state = create_game_state()\n",
    "print(state[\"grid\"])  # Should display a grid with all zeros\n",
    "b = add_random_tile(state)\n",
    "print(b)  # Should display True\n",
    "print(state[\"grid\"])  # Should display a grid with one random tile (2 or 4)\n",
    "print(\"---\")\n",
    "\n",
    "b = add_random_tile(state)\n",
    "print(b)  # Should display True\n",
    "print(state[\"grid\"])  # Should display two random tiles\n",
    "print(\"---\")\n",
    "\n",
    "# Count non-zero tiles\n",
    "count = sum(1 for row in state[\"grid\"] for cell in row if cell != 0)\n",
    "print(f\"Number of tiles: {count}\")  # Should display 2\n",
    "print(\"---\")\n",
    "\n",
    "\n",
    "state[\"grid\"] = [[2, 4, 2, 4],\n",
    "                 [4, 2, 4, 2],\n",
    "                 [2, 4, 2, 4],\n",
    "                 [4, 2, 4, 2]]\n",
    "print(state[\"grid\"])  # Filled random grid\n",
    "b = add_random_tile(state)\n",
    "print(state[\"grid\"])  # Should display the same grid\n",
    "print(b)  # Should display False\n",
    "\n",
    "print(\"---\")\n",
    "update_score(state)\n",
    "print(state[\"score\"]) # Should display 48\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "e3f196a9-5f09-4f1b-8d5f-83a6445e2ddc",
   "metadata": {},
   "source": [
    "## Step 1.3: Merging a Line to the Left\n",
    "\n",
    "**Goal:** Implement the logic for moving and merging a **single** line.\n",
    "\n",
    "**Suggested Algorithm:**\n",
    "- Extract non-zero values\n",
    "- Merge adjacent identical values\n",
    "- Fill with zeros on the right\n",
    "\n",
    "**To Implement:**"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "bd9d0577-b520-4cc2-8482-85c9dd667396",
   "metadata": {},
   "outputs": [],
   "source": [
    "def merge_line_left(line):\n",
    "    \"\"\"\n",
    "    Moves and merges tiles in a line to the left.\n",
    "    \n",
    "    Args:\n",
    "        line (list): A grid line\n",
    "    \n",
    "    Returns:\n",
    "        tuple: new_line\n",
    "    \n",
    "    Example:\n",
    "        [2, 0, 2, 4] -> [4, 4, 0, 0]\n",
    "        [2, 2, 4, 4] -> [4, 8, 0, 0]\n",
    "    \"\"\"\n",
    "    pass"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "5c0235c2-1ca2-496f-874e-0685a4b68d98",
   "metadata": {},
   "source": [
    "**Tests to Perform:**\n",
    "\n",
    "```python\n",
    "# Test 3: Line merging\n",
    "result = merge_line_left([2, 0, 2, 4])\n",
    "print(result)  # [4, 4, 0, 0]\n",
    "\n",
    "result = merge_line_left([2, 2, 4, 4])\n",
    "print(result)  # [4, 8, 0, 0]\n",
    "\n",
    "result = merge_line_left([2, 4, 8, 16])\n",
    "print(result)  # [2, 4, 8, 16]\n",
    "\n",
    "result = merge_line_left([0, 0, 0, 0])\n",
    "print(result)  # [0, 0, 0, 0]\n",
    "```"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "1ed16b12-8bb5-48d5-b523-4c286ce7847f",
   "metadata": {},
   "outputs": [],
   "source": []
  },
  {
   "cell_type": "markdown",
   "id": "b1555796-ac0b-440d-abfe-d0de7acfa5f3",
   "metadata": {},
   "source": [
    "## Step 1.4: Grid Transformations\n",
    "\n",
    "**Goal:** Implement utility functions to manipulate the grid.\n",
    "\n",
    "**To Implement:**"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "id": "13c8c808-3abe-4c2e-8774-0621667a116e",
   "metadata": {},
   "outputs": [],
   "source": [
    "def transpose_grid(grid):\n",
    "    \"\"\"\n",
    "    Returns the transpose of the grid.\n",
    "    \n",
    "    Args:\n",
    "        grid (list): Grid to transpose\n",
    "    \n",
    "    Returns:\n",
    "        list: Transposed grid\n",
    "    \"\"\"\n",
    "    pass\n",
    "\n",
    "def reverse_grid_rows(grid):\n",
    "    \"\"\"\n",
    "    Returns a grid with reversed rows.\n",
    "    \n",
    "    Args:\n",
    "        grid (list): Original grid\n",
    "    \n",
    "    Returns:\n",
    "        list: Grid with reversed rows\n",
    "    \"\"\"\n",
    "    pass"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "9124a84b-5598-4657-863d-e9427f2f3e3f",
   "metadata": {},
   "source": [
    "**Tests to perform:**\n",
    "\n",
    "```python\n",
    "# Test 4: Transformations\n",
    "grid = [[1, 2, 3, 4],\n",
    "        [5, 6, 7, 8],\n",
    "        [9, 10, 11, 12],\n",
    "        [13, 14, 15, 16]]\n",
    "\n",
    "transposed = transpose_grid(grid)\n",
    "print(transposed)  # [[1, 5, 9, 13], [2, 6, 10, 14], [3, 7, 11, 15], [4, 8, 12, 16]]\n",
    "\n",
    "reversed_grid = reverse_grid_rows(grid)\n",
    "print(reversed_grid)  # [[4, 3, 2, 1], [8, 7, 6, 5], [12, 11, 10, 9], [16, 15, 14, 13]]\n",
    "```"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "ddcfd27e-a0a0-44d9-a23f-eb6fac08cc68",
   "metadata": {},
   "outputs": [],
   "source": []
  },
  {
   "cell_type": "markdown",
   "id": "b50c6a70-ec41-4752-afb2-abd72ee718f4",
   "metadata": {},
   "source": [
    "## Step 1.5: Movements in 4 Directions\n",
    "\n",
    "**Goal:** Implement movement functions for each direction.\n",
    "\n",
    "**Tips:**\n",
    "- For move_right: reverse rows, apply move_left, reverse again\n",
    "- For move_up: transpose, apply move_left, transpose again\n",
    "- For move_down: transpose, apply move_right, transpose again\n",
    "\n",
    "**To Implement:**"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "id": "7363e26c-c5d7-4d96-a93a-0c13a11b6550",
   "metadata": {},
   "outputs": [],
   "source": [
    "def move_left(state):\n",
    "    \"\"\"\n",
    "    Returns a new state after moving left.\n",
    "    \n",
    "    Args:\n",
    "        state (dict): Current game state\n",
    "    \n",
    "    Returns:\n",
    "        dict: New state after the move\n",
    "    \"\"\"\n",
    "    pass\n",
    "\n",
    "def move_right(state):\n",
    "    \"\"\"Returns a new state after moving right.\"\"\"\n",
    "    pass\n",
    "\n",
    "def move_up(state):\n",
    "    \"\"\"Returns a new state after moving up.\"\"\"\n",
    "    pass\n",
    "\n",
    "def move_down(state):\n",
    "    \"\"\"Returns a new state after moving down.\"\"\"\n",
    "    pass"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "eaba643b-192b-4b13-8897-6ed740d92b04",
   "metadata": {},
   "source": [
    "**Tests to perform:**\n",
    "\n",
    "```python\n",
    "# Test 5: Movements\n",
    "state = create_game_state()\n",
    "state['grid'] = [\n",
    "    [2, 0, 2, 0],\n",
    "    [4, 4, 0, 0],\n",
    "    [0, 0, 8, 8],\n",
    "    [2, 2, 2, 2]\n",
    "]\n",
    "print(f\"Initial grid:    {state['grid']}\")\n",
    "b = move_left(state)\n",
    "print(f\"Grid after left: {state['grid']}\") # [[4, 0, 0, 0], [8, 0, 0, 0], [16, 0, 0, 0], [4, 4, 0, 0]]\n",
    "print(f\"Score: {state['score']}\") # 36\n",
    "print(f\"Grid has changed: {b}\") # True\n",
    "print(\"---\")\n",
    "\n",
    "print(f\"Initial grid:     {state['grid']}\")\n",
    "b = move_right(state)\n",
    "print(f\"Grid after right: {state['grid']}\") # [[0, 0, 0, 4], [0, 0, 0, 8], [0, 0, 0, 16], [0, 0, 0, 8]]\n",
    "print(f\"Score: {state['score']}\") # 36\n",
    "print(f\"Grid has changed: {b}\") # True\n",
    "print(\"---\")\n",
    "\n",
    "\n",
    "state['grid'] = [\n",
    "    [0, 0, 2, 2],\n",
    "    [4, 4, 2, 4],\n",
    "    [0, 0, 8, 8],\n",
    "    [2, 4, 2, 2]\n",
    "]\n",
    "print(f\"Initial grid:  {state['grid']}\")\n",
    "b = move_up(state)\n",
    "print(f\"Grid after up: {state['grid']}\") # [[4, 8, 4, 2], [2, 0, 8, 4], [0, 0, 2, 8], [0, 0, 0, 2]]\n",
    "print(f\"Score: {state['score']}\") # 44\n",
    "print(f\"Grid has changed: {b}\") # True\n",
    "print(\"---\")\n",
    "\n",
    "print(f\"Initial grid:    {state['grid']}\")\n",
    "b = move_down(state)\n",
    "print(f\"Grid after down: {state['grid']}\") # [[0, 0, 0, 2], [0, 0, 4, 4], [4, 0, 8, 8], [2, 8, 2, 2]]\n",
    "print(f\"Score: {state['score']}\") # 44\n",
    "print(f\"Grid has changed: {b}\") # True\n",
    "print(\"---\")\n",
    "\n",
    "\n",
    "\n",
    "state[\"grid\"] = [\n",
    "    [2, 4, 2, 4],\n",
    "    [4, 2, 4, 2],\n",
    "    [2, 4, 2, 4],\n",
    "    [4, 2, 4, 2]\n",
    "]\n",
    "print(f\"Initial grid:    {state['grid']}\")\n",
    "b = move_left(state)\n",
    "print(f\"Grid after left: {state['grid']}\") # [[0, 0, 0, 2], [0, 0, 4, 4], [4, 0, 8, 8], [2, 8, 2, 2]]\n",
    "print(f\"Grid has changed: {b}\") # False\n",
    "print(\"---\")\n",
    "```"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "1b5eac0f-ecaf-4335-9493-7ab137705720",
   "metadata": {},
   "outputs": [],
   "source": []
  },
  {
   "cell_type": "markdown",
   "id": "ea725f50-1397-44f4-b4a3-5b58ede60282",
   "metadata": {},
   "source": [
    "## Step 1.6: End Game Detection\n",
    "\n",
    "**Goal:** Detect when no moves are possible.\n",
    "\n",
    "**To Implement:**"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "id": "36927331-bc77-4029-ac2b-b821ff4df2b1",
   "metadata": {},
   "outputs": [],
   "source": [
    "def can_move(grid):\n",
    "    \"\"\"\n",
    "    Checks if at least one move is possible.\n",
    "    \n",
    "    Args:\n",
    "        grid (list): Grid to check\n",
    "    \n",
    "    Returns:\n",
    "        bool: True if a move is possible, False otherwise\n",
    "    \"\"\"\n",
    "    pass\n",
    "\n",
    "def update_game_over(state):\n",
    "    \"\"\"\n",
    "    Returns a new state with game_over updated.\n",
    "    \n",
    "    Args:\n",
    "        state (dict): Current state\n",
    "    \n",
    "    Returns:\n",
    "        dict: New state with updated game_over\n",
    "    \"\"\"\n",
    "    pass"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c785ee18-1984-4599-a10a-e8f85d60f896",
   "metadata": {},
   "source": [
    "**Tests to perform:**\n",
    "\n",
    "```python\n",
    "\n",
    "# Test 6: End detection\n",
    "grid_with_moves = [\n",
    "    [2, 4, 2, 4],\n",
    "    [4, 2, 4, 2],\n",
    "    [2, 4, 2, 4],\n",
    "    [4, 2, 4, 0]  # Empty cell\n",
    "]\n",
    "print(can_move(grid_with_moves))  # True\n",
    "\n",
    "grid_no_moves = [\n",
    "    [2, 4, 2, 4],\n",
    "    [4, 2, 4, 2],\n",
    "    [2, 4, 2, 4],\n",
    "    [4, 2, 4, 2]\n",
    "]\n",
    "print(can_move(grid_no_moves))  # False\n",
    "\n",
    "grid_with_vertical_moves = [\n",
    "    [4, 8, 2, 8],\n",
    "    [4, 2, 8, 2],\n",
    "    [2, 8, 2, 8],\n",
    "    [8, 2, 8, 2]\n",
    "]\n",
    "print(can_move(grid_with_vertical_moves))  # True\n",
    "\n",
    "grid_with_horizontal_moves = [\n",
    "    [4, 4, 2, 8],\n",
    "    [8, 2, 8, 2],\n",
    "    [2, 8, 2, 8],\n",
    "    [8, 2, 8, 2]\n",
    "]\n",
    "print(can_move(grid_with_horizontal_moves))  # True\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "61fa49ad-0a02-4fb9-ac60-0a746b30cb3b",
   "metadata": {},
   "source": [
    "## Step 1.7: Main Game Function\n",
    "\n",
    "**Goal:** Encapsulate all the logic for one move.\n",
    "\n",
    "**To Implement:**"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 41,
   "id": "64c443ae-d85c-416c-a8c4-e1fe5a842ca6",
   "metadata": {},
   "outputs": [],
   "source": [
    "def play_move(state, direction):\n",
    "    \"\"\"\n",
    "    Performs a move in the given direction.\n",
    "    \n",
    "    Args:\n",
    "        state (dict): Current game state\n",
    "        direction (str): 'up', 'down', 'left', 'right'\n",
    "    \n",
    "    Returns:\n",
    "        dict: New state after the move\n",
    "    \"\"\"\n",
    "    # 1. Perform the move according to direction\n",
    "    # 2. If the grid has changed:\n",
    "    #    - Add a new tile\n",
    "    #    - Check game over\n",
    "    # 3. Return the new state\n",
    "    pass"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a0c8cfb8-812b-4045-89bf-55d9f6f7db79",
   "metadata": {},
   "source": [
    "**Tests to perform:**\n",
    "\n",
    "```python\n",
    "# Test 7: Complete game\n",
    "state = create_game_state()\n",
    "state[\"grid\"] = [\n",
    "    [4, 4, 2, 8],\n",
    "    [8, 2, 8, 2],\n",
    "    [2, 8, 2, 8],\n",
    "    [8, 2, 8, 2]\n",
    "]\n",
    "\n",
    "print(\"Initial state:\")\n",
    "print(state[\"grid\"])\n",
    "\n",
    "\n",
    "play_move(state, 'left')\n",
    "print(\"\\nAfter left move:\")\n",
    "print(state[\"grid\"]) # Should be: [[8, 2, 8, X], [8, 2, 8, 2], [2, 8, 2, 8], [8, 2, 8, 2]] with x = 2 or 4\n",
    "print(f\"Game Over: {state['game_over']}\") # Should be False\n",
    "```"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "2eac492a-518e-4a24-937d-5c8fbc25630f",
   "metadata": {},
   "outputs": [],
   "source": []
  },
  {
   "cell_type": "markdown",
   "id": "8532da40-54b3-4c82-a20c-23e272c91928",
   "metadata": {},
   "source": [
    "# Part 2: The View with `rich` and `readchar`\n",
    "\n",
    "From this step, you will need to use a terminal to run the program. In other case, the command from `rich` and `readchar` libraries will not work.\n",
    "\n",
    "## Step 2.1: Install the libraries\n",
    "\n",
    "With **Thonny**, in the `tools` menu, you can open a configured terminal using `open system shell`. In the terminal, launch the two following commands to install the libraries.\n",
    "```bash\n",
    "pip3 install rich\n",
    "pip3 install readchar\n",
    "```\n",
    "\n",
    "Create a `view.py` module containing the following import instructions.\n",
    "\n",
    "```python\n",
    "import rich.box\n",
    "from rich.console import Console\n",
    "from rich.table import Table\n",
    "from rich.text import Text\n",
    "import readchar\n",
    "```\n",
    "\n",
    "Open a terminal and launch the command `python3 view.py`. If there is no error, then the module are correctly installed.\n",
    "\n",
    "\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "4497dbc4-418b-4333-82b1-c74e0de529b3",
   "metadata": {},
   "source": [
    "## Step 2.2: Read a character from the keyboard\n",
    "\n",
    "In the module `view.py` add the following function and test it.\n",
    "\n",
    "```python\n",
    "\n",
    "def get_player_input():\n",
    "    \"\"\"\n",
    "    Waits for and returns player input.\n",
    "\n",
    "    Returns:\n",
    "        str: Direction ('up', 'down', 'left', 'right') or 'quit'\n",
    "    \"\"\"\n",
    "    try:\n",
    "        key = readchar.readkey()\n",
    "\n",
    "        # Map keys to directions\n",
    "        key_mapping = {\n",
    "            readchar.key.UP: 'up',\n",
    "            readchar.key.DOWN: 'down',\n",
    "            readchar.key.LEFT: 'left',\n",
    "            readchar.key.RIGHT: 'right',\n",
    "            'q': 'quit',\n",
    "            'Q': 'quit'\n",
    "        }\n",
    "\n",
    "        return key_mapping.get(key, None)\n",
    "\n",
    "    except:\n",
    "        return None\n",
    "\n",
    "\n",
    "def test_input():\n",
    "    \"\"\"\n",
    "    Waits for an arrow key until \"q\" or \"Q\" is input.\n",
    "    \"\"\"\n",
    "    k = get_player_input()\n",
    "    while k != \"quit\":\n",
    "        print(k)\n",
    "        k = get_player_input()\n",
    "    print(k)\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "18e798a6-a11c-4cb2-815a-5fb58165b206",
   "metadata": {},
   "source": [
    "## Step 2.3: Grid Display\n",
    "\n",
    "We provide functions to help you to represent the grid using `rich` module. Copy/paste the following code into the `view.py` module.\n",
    "\n",
    "```python\n",
    "# Color palette for tiles\n",
    "TILE_STYLES = {\n",
    "    0: \"dim white on grey23\",  # Empty\n",
    "    2: \"black on grey78\",  # 2\n",
    "    4: \"black on grey74\",  # 4\n",
    "    8: \"white on dark_orange\",  # 8\n",
    "    16: \"white on orange1\",  # 16\n",
    "    32: \"white on red\",  # 32\n",
    "    64: \"white on red3\",  # 64\n",
    "    128: \"black on yellow\",  # 128\n",
    "    256: \"black on gold1\",  # 256\n",
    "    512: \"black on yellow1\",  # 512\n",
    "    1024: \"black on gold3\",  # 1024\n",
    "    2048: \"white on yellow1 bold\",  # 2048\n",
    "}\n",
    "\n",
    "\n",
    "def create_console():\n",
    "    \"\"\"\n",
    "    Creates a Rich console instance.\n",
    "\n",
    "    Returns:\n",
    "        Console: Console instance\n",
    "    \"\"\"\n",
    "    return Console()\n",
    "\n",
    "\n",
    "def get_tile_style(value):\n",
    "    \"\"\"\n",
    "    Returns the style (color, background) for a tile value.\n",
    "\n",
    "    Args:\n",
    "        value (int): Tile value\n",
    "\n",
    "    Returns:\n",
    "        str: Rich style string\n",
    "    \"\"\"\n",
    "    if value not in TILE_STYLES:\n",
    "        # For values > 2048\n",
    "        return \"white on magenta bold\"\n",
    "    return TILE_STYLES[value]\n",
    "\n",
    "\n",
    "def create_grid_table(state):\n",
    "    \"\"\"\n",
    "    Creates a Rich Table representing the game grid.\n",
    "\n",
    "    Args:\n",
    "        state (dict): Game state\n",
    "\n",
    "    Returns:\n",
    "        Table: Styled Rich table\n",
    "    \"\"\"\n",
    "\n",
    "    grid = state['grid']\n",
    "\n",
    "    # Create table without headers\n",
    "    table = Table(show_header=False, show_edge=True, show_lines=True, pad_edge=False,\n",
    "                  box=rich.box.HEAVY_EDGE, padding=(0, 0))\n",
    "\n",
    "    # Add columns\n",
    "    for _ in range(len(grid)):\n",
    "        table.add_column(justify=\"center\", width=6)\n",
    "\n",
    "    # Add rows with styled tiles\n",
    "    for row in grid:\n",
    "        styled_cells = []\n",
    "        for value in row:\n",
    "            if value == 0:\n",
    "                # Empty cell\n",
    "                cell = Text(\"·\", style=get_tile_style(0))\n",
    "            else:\n",
    "                # Tile with value\n",
    "                cell = Text(str(value), style=get_tile_style(value))\n",
    "            styled_cells.append(cell)\n",
    "\n",
    "        table.add_row(*styled_cells)\n",
    "\n",
    "    return table\n",
    "```\n"
   ]
  },
  {
   "attachments": {
    "d9d13985-66fb-4906-b979-fab2bbcd0960.png": {
     "image/png": "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"
    }
   },
   "cell_type": "markdown",
   "id": "2c6694e2-74d7-4dea-8fde-8bc776292a1a",
   "metadata": {},
   "source": [
    "**Tests to perform:**\n",
    "\n",
    "The following function allow you to create a test grid and display it on a terminal. Remember, you should use a terminal to execute your programm. Do not use the `run current script` buton (green arrow).\n",
    "\n",
    "```python\n",
    "def test_grid_display():\n",
    "    \"\"\"\n",
    "    Test function to display a sample grid.\n",
    "    \"\"\"\n",
    "    state = {\n",
    "        'grid': [\n",
    "            [2, 0, 0, 2],\n",
    "            [4, 4, 0, 0],\n",
    "            [0, 0, 8, 8],\n",
    "            [16, 0, 0, 16]\n",
    "        ],\n",
    "        'score': 0,\n",
    "        'game_over': False\n",
    "    }\n",
    "\n",
    "    console = create_console() # Create a console instance\n",
    "    table = create_grid_table(state) # Create the grid table\n",
    "    console.clear() # Clear the console\n",
    "    console.print(table) # Print the table to the console \n",
    "```\n",
    "\n",
    "You should see something like that.\n",
    "\n",
    "![Capture d’écran 2025-12-01 à 19.34.26.png](attachment:d9d13985-66fb-4906-b979-fab2bbcd0960.png)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "48479334-e22a-4202-8563-f219c697d5c9",
   "metadata": {},
   "source": [
    "# Part 3: The Controller (Interaction Management)\n",
    "\n",
    "You have now all functionalities to implement the controller of the game.\n",
    "\n",
    "## Step 3.1: Keyboard Input Management with Blessed\n",
    "\n",
    "**Goal:** Create a `controller.py` module and use functions from `model` and `view` to implement the game.\n",
    "\n",
    "**To Implement:**"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c5f0968b-767e-467b-9195-a9e8428c7c81",
   "metadata": {},
   "outputs": [],
   "source": [
    "import model\n",
    "import view\n",
    "\n",
    "def game_loop():\n",
    "    \"\"\"\n",
    "    Main game loop for 2048. Create a new game state, add two initial tiles,\n",
    "    and repeatedly get player input to play moves until the game is over. Then quits and display the player score.\n",
    "\n",
    "    :return:\n",
    "    \"\"\"\n",
    "    pass\n",
    "\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a3de6285-e2a5-4ef2-83d4-94b7857f0db5",
   "metadata": {},
   "source": [
    "# Part 4: Show your skills\n",
    "\n",
    "Your project is now complete in its minimal version.\n",
    "\n",
    "You can now show your capabilities. You can implement any new features you choose. For example:\n",
    "- The ability to quit and resume a game later with a save file.\n",
    "- A high score table that is saved between launches.\n",
    "- A graphical interface using Pygame.\n",
    "- An AI that plays for you.\n",
    "- ...\n",
    "\n",
    "However, here are a few constraints:\n",
    "- The model/view/controller structure must be maintained.\n",
    "- The model must not change unless a specific reason is provided. It can, however, be expanded.\n",
    "- Documentation in the form of a README.txt file must document and explain the features you have chosen to implement.\n",
    "\n",
    "At the end of your work, you will upload the directory containing all your source code and documentation in ZIP format (only zip, no 7zip or rar, or anything else). You will find on EDUNAO a link to upload your project. The deadline is the 4th january 2026 at 23h59.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "24332c78-4b01-41d9-a2f3-f3d3a5d23ff0",
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.11.13"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
